<?php

//============================================================================
//
// compare_classes.php
// -------------------
//
// Classes for server comparision
//
// Part of s-audit. (c) 2011 SearchNet Ltd
//  see http://snltd.co.uk/s-audit for licensing and documentation
//
//============================================================================

//============================================================================
// SERVER LIST PAGE

class compareListPage extends audPage {

	// This is the default landing page for the compare servers page. (As
	// opposed to comparePage, which is used for the comparisons proper.)

    protected $no_class_link = true;
		// There's no "this class" documentation link

	protected $group;
		// The audit group we're looking at

	protected $ff;
		// Path to friends file

	protected $map;
		// as usual

	// We need the "compare" stylesheet

	protected $styles = array("basic.css", "audit.css", "compare.css");

	public function __construct($title, $map)
	{
		if (!isset($_GET["g"]))
			die("no group");

		$this->group = $_GET["g"];
		$this->map = $map;

		$this->ff = AUDIT_DIR . "/$this->group/friends.txt";

		// Expand the map. We need all the local zone names, so create the
		// servers[] array in the map, just like GetServers normally does

		foreach($map->map as $global=>$file) {
			$this->map->servers[$global] =
			array_slice(array_unique(preg_grep("/^hostname=/",
			file($file))), 1);
		}

		parent::__construct($title, false);
	}

	public function ff_list()
	{
		// Say whether or not we're using a friends file. If we are, display
		// the list

		$ff_link = "<a href=\"" . DOC_URL .
		"/04_extras/friends.php\">friends file</a>";

		$ret = "\n\n<p class=\"center\">";
		
		// If we don't have a friends file, we're done

		if (!file_exists($this->ff))  {
			$ret .= "You do not have a $ff_link for this audit group.</p>";
		}
		else {
			$ret .= "The following server comparisons are defined in the
			$ff_link at <tt>$this->ff</tt>.</p>\n\n<ul>";

			// Read in the friends file and make an array of the pairs, and
			// possibly descriptions, inside it. 

			foreach(file($this->ff) as $row) {

				// clear some vars

				$desc = $pass = $trimmed = false;

				// Discard anything that doesn't start with a letter.  Hostnames
				// have to start with a letter

				if (!preg_match("/^\w/", $row)) continue;
		
				// Split off the description, if we have one

				$a = explode(":", $row, 2);

				$desc = (isset($a[1])) ? $a[1] : false;

				// a[0] has the friends, a[1] has the comment

				$friends = explode(",", $a[0]);

				// Discard friendless loners

				if (count($friends) == 1) continue;

				// Make sure all the friends exist in the map. If they don't,
				// make an array called "missing".
			
				foreach($friends as $h) {
					$h = trim($h);

					if (!$this->map->has_data($h)) {
						$missing[] = array($friends, $desc);
						$pass = true;
						break;
					}
					else
						$trimmed[] = $h;

				}

				// if $pass is true, we take no further action on this group of
				// friends

				if ($pass) continue;

				// Create the list entry for this group of friends

				$ret .= "\n  <ul><a href=\"" . $_SERVER["PHP_SELF"] . "?g="
				. $this->group . "&amp;d=" . urlencode(serialize($trimmed))
				. "\">" . $trimmed[0];

				for ($i = 1; $i < count($trimmed) - 1; $i++)
					$ret .= ", " . $trimmed[$i];

				$ret .= " and " . $trimmed[count($trimmed) - 1] . "</a>";

				if ($desc) $ret .= " ($desc)";

				$ret .= "</ul>";
			}

			if (isset($missing)) {
				$ret .= "<p class=\"center\">The following friends are defined,
				but we do not have sufficient data to compare them.</p>\n<ul>";

				foreach ($missing as $friends) {
					$hosts = $friends[0];
					$nhosts = count($hosts);

					$ret .= "\n<li>" . new singleServerLink($hosts[0]);

					for($i = 1; $i < $nhosts - 1; $i++) {
						$ret .= ", " . new singleServerLink($hosts[$i]);
					}

					$ret .= " and " . new singleServerLink($hosts[$i]);

					if (isset($friends[1])) $ret .= " (" . $friends[1] . ")";

					$ret .= "</li>";
				}

				$ret .= "\n</ul>";

			}

		}

		return $ret . new compareCyc($this->map->list_all(), $this->group);
	}

}

//----------------------------------------------------------------------------

class compareCyc {

	// The dual cycle gadget on the server comparison page

	private $html;
		// The HTML generated by the constructor

    public function __construct($z_list, $group, $z1 = false, $z2 = false)
    {
		// This is the bar at the bottom of the screen that lets you choose
		// the servers to compare from cycle gadgets.
		
		$h = new html;

        $this->html = "<p class=\"center\">Select a pair of servers or zones
		to compare with the following gadgets.</p>"
		. "\n<div id=\"cycle_row\">"
        . $h->dialog_form($_SERVER["PHP_SELF"])
        . $h->dialog_submit("c", "compare")
        . $h->dialog_cycle("hosts[0]", $z_list, $z1, false) . " with "
        . $h->dialog_cycle("hosts[1]", $z_list, $z2, false)
		. $h->dialog_hidden("g", $group)
        . "</form>\n</div>";
    }

	public function __toString() 
	{
		return $this->html;
	}

}

//============================================================================
// COMPARISON PAGE

class comparePage extends audPage {

	// The basic HTML template for the page used for the actual comparison.

    protected $no_class_link = true;
		// There's no "this class" documentation link

	protected $styles = array("basic.css", "audit.css", "compare.css");

	// We want some Javascript to show and hide common data

	protected $my_js = "compare.js";
	
}

//----------------------------------------------------------------------------

class compareView {
	
	// This class groups together functions needed to compare two servers,
	// and display the results of that comparison. It works like the
	// serverView class does for single server pages.

    private $a;
    private $b;
        // The two servers on which we're doing an a/b comparison

	private $classes;
		// Audit classes we're going to compare

    public function __construct($data, $map)
    {
		// Call this constructor to create the whole compare grid.

		$this->map = $map;

		// Populate the $fields array with the hostnames of the servers to
		// compare and get a list of classes. Servers should always have the
		// same classes, but make sure we get everything anyway.

		$classes = array();

		// create $this->data, which puts the class data for each machine in
		// the same level of the data structure
		//
		// [class] -> [server_a]
		//            [server_b]

		foreach($data as $host=>$h_data) {
			$this->fields[] = $host;

			foreach($h_data as $class=>$c_data) {

				$this->data[$class][$host] = $c_data;

				if (!in_array($class, $classes))
					$classes[] = $class;
			}

		}

		$this->classes = $classes;
    }

    public function show_grid()
    {
		// The grid in this case is a list of tables, one for each audit
		// type. Each table is created by its own class

        $ret = false;

        foreach($this->classes as $type) {

            $class = (class_exists("compare$type"))
				? "compare$type"
                : "compareGeneric";

			// Call the class with its name, the data for servers, and the
			// map

            $ret .= new $class($type, $this->data[$type], $this->map);
        }

        return $ret;
    }

}

//----------------------------------------------------------------------------
// Generic compare. Every audit class has its own compareClass which is an
// extension of this

class compareGeneric {

	// Some fields need pre-processing. This could be because they, like
	// NICs, come machine-parseable so aren't easy to read, or it could be
	// because, like network port information, we discard some data. For
	// that reason we can create preproc_field() methods. If they exist,
	// they're called. 

	private $hosts;

	protected $cols;
		// columns, excluding the key - size of the hosts array

	private $html;

	protected $no_colour = array();
		// Don't colour these even if they're different. Used for things
		// like IP addresses, or for numbers of packages, where one number
		// isn't necessarily "better" than the other.

	protected $no_compare = array();
		// Don't compare fields in this array. You won't see them on the
		// grid at all

	protected $sparse_list = array();
		// Fields which are displayed with "holes" in them

	private $width = "width:80em";
		// The width of the compare grid (all columns combined)

	private $colwidth;
		// the % width of each column in the comparison table

	public function __construct($type, $data, $map)
	{
		// Start off by printing the header

		if (!isset($this->disp_name))
			$this->disp_name = $type;

		$this->html = "\n\n<table align=\"center\" style=\"$this->width\">"
		. "\n<tr><td class=\"nocol\"><h1>" . ucfirst($this->disp_name)
		. " audit comparison</h1></td>"
		. "</tr>\n</table>\n";
		
		// If all we have is hostname and audit completed, we're done

		$i = 0;

		foreach($data as $svr => $dat) {
			$i += count($dat);
		}

		if ($i == 4) {
			$this->html .= "<p class=\"center\">No data to compare.</p>";
			return;
		}

		$this->map = $map;

		// Get the hostnames and all the rows we're going to compare

		$this->hosts = array_keys($data);
		$this->cols = count($this->hosts);

		// This is slow and messy, but I can't think of a better way to do
		// it right now

		$rows = array();

		foreach($this->hosts as $host) {
			
			foreach($data[$host] as $key=>$value) {

				if (!in_array($key, $rows)) $rows[] = $key;
			}

		}

		// We may need to move "audit completed" to the last row

		if (end($rows) != "audit completed") {
			$k = array_search("audit completed", $rows);
			unset($rows[$k]);
			$rows[] = "audit completed";
		}

		if (count($data) != 2) {

			$data["unknown host"] = array(
				"hostname" => array("unknown host"),
				"audit completed" => array("no audit")
			);

			$this->hosts[] = "unknown host";
		}

		$this->data = $data;
		$this->rows = $rows;
		$this->colwidth = round(100 / $this->cols) . "%";
		$this->html .= $this->compare_class();

		if (method_exists($this, "cmp_key")) {
			$this->html .= "\n\n<table align=\"center\" style=\""
			. $this->width
			. "\">\n<tr><td class=\"nocol\"><p class=\"center\">"
			. $this->cmp_key() .  "</p></td></tr></table>";
		}

	}

	protected function compare_class()
	{
		// Print the comparison tables for the class

		$ret = "\n\n<table cellspacing=\"1\" class=\"audit\" align=\"center\""
		. " style=\"$this->width\">\n";

		// Step through each row

		foreach($this->rows as $row) {

			// Some fields have can special functions which we use to
			// compare them.  The rest use the compare_generic() method.
			// Work out what that method should be called, see if it exists,
			// and call

			$method_base = preg_replace("/\s/", "_", $row);
			$pre_method = "preproc_$method_base";
			$cmp_method = "compare_$method_base";
			$post_method = "postproc_$method_base";

			// Application and tool have a preproc_generic

			if (!method_exists($this, $pre_method) && method_exists($this,
				"preproc_generic"))
				$pre_method = "preproc_generic";

			$cmp_data =$this->get_cmp_data($row);

			if (method_exists($this, $pre_method)) {

				// We have at least two servers to compare, so there are at
				// least two arrays of data to process. Put a loop here to
				// do them all, rather than having to loop in every single
				// preproc_() method

				$proc_arr = array();

				foreach($cmp_data as $svr) {
					$proc_arr[] = $this->$pre_method($svr);
				}

				$cmp_data = $proc_arr;
			}

			// the $no_compare[] array may tell us we don't wish to compare
			// this row.

			if (in_array($row, $this->no_compare)) {
				$rowdat = "don't compare";
				continue;
			}
			elseif(method_exists($this, $cmp_method)) {
				$rowdat = $this->$cmp_method($cmp_data);
			}
			else {

				$rowdat = (in_array($row, $this->sparse_list))
					? $this->compare_sparse($cmp_data, $row)
					: $this->compare_generic($cmp_data, $row);
			}

			// Do we need to post-process the compared data?

			if (method_exists($this, $post_method))
				$rowdat = $this->$post_method($rowdat);

			// Work through each line of data for this row. The left-most
			// column will be green if all the rows are identical, red if
			// they're not. We build up table rows in the $d variable.

			$diffs = 0;
				// Count how many differences there are in this row's data
		
			$rr = array(); // an array of rows of HTML

			foreach($rowdat as $line) {

				// the "all" element will be set for common data. The "call"
				// element will be set if we're colouring the row

				if (isset($line["all"])) {

					$cl = (isset($line["call"]))
						? $line["call"]
						: false;

					$rr[] =  new Cell($line["all"], $cl, false, false,
					$this->cols);

				}
				else {
					$diffs++;

					if (!isset($line["ca"])) $line["ca"] = false;
					if (!isset($line["cb"])) $line["cb"] = false;

					$rr[] = new Cell($line["a"], $line["ca"], false,
					$this->colwidth) . new Cell($line["b"], $line["cb"], false,
					$this->colwidth);
				}

			}

			if ($row == "hostname") {
				$ret .= "\n\n<tr><td class=\"blank\"></td>$rr[0]";
			}
			else {

				// If the "difference" variable is set, we found a
				// difference in this row. If not, make the whole row
				// "disappearable" by Javascript. Colour the cell red or
				// green too.

				if ($diffs > 0) {
					$kcl = "solidred";
					$ret .= "\n<tr>";
				}
				else {
					$ret .=
					"\n<tr class=\"hide\" style=\"display: table-row\">";
					$kcl = "solidgreen";
				}

				$ret .= "<th class=\"$kcl\" ";
			
				if (count($rowdat) > 1)
					$ret .= "rowspan=\"" . count($rowdat) . "\"";

				$ret .= ">$row</th>";

				for ($i = 0; $i < count($rr); $i++) {

					if ($i > 0) {

						if ($diffs == 0 && preg_match("/colspan=/",
							$rr[$i]))
							$ret .=
					"\n<tr class=\"hide\" style=\"display: table-row\">";
						else
							$ret .= "\n  <tr>";
					}
					//else

					
					$ret .= $rr[$i] . "</tr>";
				}

			}
			
		}

		return $ret . "\n</table>";
	}

	protected function get_cmp_data($row)
	{
		// Get the "row" data for all hosts, and put it in an array. For
		// now, this only operates on two hosts. May increase later, which
		// is why it's in a separate method

		$ret = array();

		foreach($this->hosts as $host) {

			$ret[] = isset($this->data[$host][$row])
				? $this->data[$host][$row]
				: array(false);
		}

		return $ret;
	}

	protected function compare_generic($data, $row)
	{
		// This is the method which does 90% of the comparisons. Returns an
		// array of arrays, each of which has these possible elements:
		// [all] = value  : both values are the same
		// [a] = value in LH column
		// [b] = value in RH column
		// [ca] = class for LH column
		// [cb] = class for RH column

		if (!isset($data[0])) $data[0] = array();
		if (!isset($data[1])) $data[1] = array();

		// First find things that are the same

		$ret = array();

		foreach(array_intersect($data[0], $data[1]) as $match) {
			$ret[] = array("all" => $match, "ca" => false, "cb" => false);
		}

		// Now get the differences looking both ways. We do the
		// array_values() call so the indicies start at zero

		$ma = array_values(array_diff($data[0], $data[1]));
		$mb = array_values(array_diff($data[1], $data[0]));

		// We have to make the arrays the same size
		
		$sa = $diffs = count($ma);
		$sb = count($mb);

		if ($sa > $sb)
			$mb = array_pad($mb, $sa, false);
		elseif ($sb > $sa) {
			$ma = array_pad($ma, $sa, false);
			$diffs = $sb;
		}

		// Do we need to get the highest version?

		if (method_exists($this, "get_highver") && !in_array($row,
			$this->no_colour))
			$hv = $this->get_highver($data);
		
		// Go through the differences, colouring (or not) as we go

		for($i = 0; $i < $diffs; $i++) {

			if (!isset($ma[$i])) $ma[$i] = false;
			if (!isset($mb[$i])) $mb[$i] = false;

			$tmp_arr = array("a" => $ma[$i], "b" => $mb[$i]);

			if (in_array($row, $this->no_colour)) {
				$tmp_arr["ca"] = $tmp_arr["cb"] = false;
			}
			elseif(isset($hv)) {

				if (preg_match("/$hv/", $ma[$i]))
					$tmp_arr["ca"] = "ver_l";
				elseif(!empty($ma[$i]))
					$tmp_arr["ca"] = "ver_o";

				if (preg_match("/$hv/", $mb[$i]))
					$tmp_arr["cb"] = "ver_l";
				elseif(!empty($mb[$i]))
					$tmp_arr["cb"] = "ver_o";

			}
			else {

				if ($this->safe_compare($ma[$i], $mb[$i])) {
					$tmp_arr["ca"] = "ver_l";
					$tmp_arr["cb"] = "ver_o";
				}
				else {
					$tmp_arr["ca"] = "ver_o";
					$tmp_arr["cb"] = "ver_l";
				}
				
			}

			$ret[] = $tmp_arr;
		}

		return $ret;
	}

	protected function compare_sparse($data)
	{
		// Make a big array of everything, sort it, then see what's in
		// what. It's not really a comparison, just separates everthing on
		// both hosts into one, the other, or both.

		$whole = $tmp_arr = $ret = array();

		$a = $data[0];
		$b = $data[1];
		
		$whole = array_merge($a, $b);

		$whole = array_unique($whole);
		natsort($whole);

		foreach($whole as $el) {
			
			if (in_array($el, $a) && in_array($el, $b))
				$tmp_arr["all"] = $el;
			elseif (in_array($el, $a))
				$tmp_arr = array("a" => $el, "b" => false);
			else
				$tmp_arr = array("a" => false, "b" => $el);

			$ret[] = $tmp_arr;
		}

		return $ret;
	}

	protected function compare_hostname($data)
	{
		// Hostname gets a special row so we can set classes

		$a = array(

			array(
				"a" => $data[0][0],
				"b" => $data[1][0],
				"ca" => "keyhead",
				"cb" => "keyhead"
			)

		);

		return $a;
	}

	protected function compare_audit_completed($data)
	{
		// Work out the time difference between oldest and newest audits,
		// and colour accordingly. First row spans both columns and tells
		// you the time difference between the oldest and newest audits.
		// Seconds row displays the time of each

		$d_arr = array();

		$now = time();

		foreach($data as $datum) {
			$d = preg_split("/[:\s\/]+/", $datum[0]);

			if (isset($d[5]))
				$d_arr[] = mktime($d[0], $d[1], $d[2], $d[4], $d[3], $d[5]);
			else
				$broken = true;
		}
		
		sort($d_arr);
		reset($d_arr);
		$first = current($d_arr);
		$last = end($d_arr);
		$diff = $last - $first;

		// If we're comparing two dates, say they're x apart, if it's more
		// than two, talk about a spread

		$string = (count($data) == 2)
			? "apart"
			: "spread";

		$diff_col = "green";

		if (isset($broken)) {
			$txt = "no times to compare";
			$diff_col = "red";
		}
		elseif ($diff == 0) {
			$txt = "identical times";
		}
		elseif ($diff < 60) {
			$txt = "$diff second(s) $string";
		}
		elseif ($diff < 3600) {
			$txt = round($diff / 60) . " minute(s) $string";
		}
		elseif ($diff < 216000) {
			$txt = round(($diff / 3660), 1) . " hour(s) $string";
			$diff_col = "amber";
		}
		else {
			$txt = round($diff / 216000) . " day(s) $string";
			$diff_col = "red";
		}
	
		return array(
			array("all" => "$txt", "call" => "solid$diff_col"),
			array("a" => $data[0][0], "b" => $data[1][0], "ca" => false,
			"cb" => false)
		);
	}

	public function safe_compare($a, $b)
	{
		// If you're comparing version numbers and you hit, say, 2.2.4 and
		// 2.2.11, a normal >/< type comparison will tell you 2.2.4 is the
		// later version, which it plainly isn't. This functon uses PHP's
		// natural sort algorithm to get the higher version. It also ignores
		// anything that's not part of the version string. (i.e. anything
		// after the first space)
		
		// returns true if a > b
		// returns false otherwise

		$ea = preg_replace("/ .*$/", "", $a);
		$eb = preg_replace("/ .*$/", "", $b);

		$arr = array($a, $b);
		natsort($arr);

		return (current($arr) == $a) ? false : true;
	}

	protected function preproc_bold_first_word($data, $sep = ":")
	{
		// For things that start "something:", put "something" in bold

		return preg_replace("/^(.*)($sep)/U", "<strong>$1$2</strong>",
		$data);
	}

	public function __toString()
	{
		return $this->html;
	}

}

//----------------------------------------------------------------------------
// PLATFORM AUDITS

class comparePlatform extends compareGeneric {

	protected $no_colour = array("hostname", "audit completed", "hardware",
	"virtualization", "serial number", "ALOM IP", "LOM IP", "card", "memory",
	"storage", "EEPROM", "multipath");

	protected function preproc_storage($data)
	{
		return $this->preproc_bold_first_word($data);
	}

	protected function preproc_EEPROM($data)
	{
		return $this->preproc_bold_first_word($data, "=");
	}

}

//----------------------------------------------------------------------------
// O/S AUDITS

class compareOS extends compareGeneric {

	protected $no_colour = array("hostname", "audit completed", "hostid",
	"uptime", "local zone", "distribution", "VM", "scheduler",
	"SMF services", "packages", "patches", "boot env", "publisher");

	protected $disp_name = "O/S";

	public function __construct($type, $data, $map)
	{
		// If the distributions aren't the same. don't colour the version or
		// kernel

		$dist_arr = array();

		foreach($data as $svr=>$dat) {

			if (isset($data["distribution"]))
				$dist_arr[] = $dat["distribution"];

		}

		$dists = array_unique($dist_arr);

		if (count($dists > 1)) {
			$this->no_colour[] = "version";
			$this->no_colour[] = "kernel";
			$this->no_colour[] = "release";
		}

		parent::__construct($type, $data, $map);

	}

	protected function preproc_boot_env($data)
	{
		return $this->preproc_bold_first_word($data);
	}

	protected function preproc_VM($data)
	{
		// For now I'm just going to bold the VM type and strip out any [].
		// Might do more with this in future

		$data =  $this->preproc_bold_first_word(preg_replace("/\[\]/", "",
		$data));

		// Remove the state from zones. This is helpful if you have
		// clustered zones

		foreach($data as $vm) {

			if (preg_match("/local zone:/", $vm))
				$vm = preg_replace("/\s\(.*\)/", "", $vm);

			$ret[] = $vm;
		}

		return $ret;

	}

}

//----------------------------------------------------------------------------
// NETWORK AUDITS

class compareNet extends compareGeneric {

	protected $no_colour = array("hostname", "audit completed", "NTP",
	"NFS domain", "name service", "DNS server", "port", "route", "net",
	"routing", "SNMP");

	protected $sparse_list = array("port");

	protected function preproc_name_service($data)
	{
		return $this->preproc_bold_first_word($data);
	}

	protected function preproc_route($data)
	{
		// Bold the network and remove the local IP address part (ignoring
		// default routes)

		$defs = preg_grep("/default/", $data);
		$other = preg_replace("/\s\S+\s/", " ", array_diff($data, $defs));

		return $this->preproc_bold_first_word(array_merge($other, $defs), "\s");

	}

	protected function preproc_port($data)
	{
		// As on the net audit, we may want to remove high numbered ports.
		// Process the info in the same way too

		$ret = array();

		foreach($data as $datum) {
			$a = explode(":", $datum);

			if ((defined("OMIT_PORT_THRESHOLD")) && ($a[0] >
				OMIT_PORT_THRESHOLD))
				continue;

			if (empty($a[1])) $a[1] = "-";
			if (empty($a[2])) $a[2] = "-";
				
			$ret[] = "<strong>$a[0]</strong> ($a[1]/$a[2])";
		}

		return $ret;
	}

	protected function preproc_net($data)
	{
		// For network interfaces print the NIC name, the speed, and the
		// subnet it's on (simply by changing the last octet of the IP
		// address to a zero)

		$ret = array();

		// nxge1|phys|10.2.132.170|0:21:28:b8:7:e5 |wave2-wati|1000Mb:full|||1
		//  0   | 1  |      2     |        3       |     4    |      5

		foreach($data as $datum) {
			$a = explode("|", $datum);
			$speed = "unknown speed";

			if (count($a) < 7)
				continue;

			// Split the speed/duplex into two parts

			$sa = preg_split("/:|-/", $a[5]);
			
			// Now $sa[0] is the speed, $sa[1] is the duplex. I don't want
			// the "b" on the speed.
			
			$sa[0] = str_replace("b", "", $sa[0]);
			
			// Make the speed "1G" if it's 1000M. Also look out for long
			// strings from kstat.
			
			if ($sa[0] == "1000M" || $sa[0] == "1000000000")
				$sa[0] = "1G";
			elseif ($sa[0] == "100000000")
				$sa[0] = "100M";
			elseif ($sa[0] == "10000000")
				$sa[0] = "10M";
			
			// Make the duplex part "full" if it's only "f", and "half" if
			// it's only "h"
			
			if (sizeof($sa) > 1) {
			
				if ($sa[1] == "f")
					$sa[1] = "full";
				elseif ($sa[1] == "h")
					$sa[1] = "half";
			
				$speed = "${sa[0]}bit/$sa[1] duplex";
			}

			// network

			$snet = preg_replace("/\d+$/", "0", $a[2]);

			$sspeed = ($snet != "uncabled" && $snet != "unconfigured")
				? " ($speed)"
				: false;

			// IPMP group

			$sipmp = ($a[7])
				? " '$a[7]' IPMP group"
				: false;
			
			$ret[] = "<strong>$a[0]:</strong> ${snet}${sipmp}$sspeed";
		
		}

		return $ret;
	}

	protected function cmp_key()
	{

		// If we've stripped out high numbered ports, say so

		if (defined("OMIT_PORT_THRESHOLD"))
			return "NOTE: in the &quot;port&quot; comparison, open ports
			above " .  OMIT_PORT_THRESHOLD . " are not being displayed.";
	}
}

//----------------------------------------------------------------------------
// FILESYSTEM AUDITS

class compareFS extends compareGeneric {

	protected $no_colour = array("hostname", "audit completed", "zpool",
	"metaset", "root fs", "fs", "export", "capacity", "disk group");

	protected $disp_name = "Filesystem";

	protected function preproc_zpool($data)
	{
		// Bold the pool name and bin the scrub info

		$data = preg_replace("/\(.*\) /", "", $data);
		return $this->preproc_bold_first_word($data, "\s");

	}

	protected function preproc_capacity($data)
	{
		// Bold the capacity. Might do more with this one day. Can't decide
		// now.

		return $this->preproc_bold_first_word($data, "\s");
	}

	protected function preproc_disk_group($data)
	{
		// Just bold the disk group name and show the number of disks in it.
		// Bin the rest.

		$data = preg_replace("/ .*\[(\d+ disk).*$/", " [$1(s)]", $data);
		return $this->preproc_bold_first_word($data, "\s");
	}

	protected function preproc_fs($data)
	{
		// Keep the mountpoint, filesystem type and device. Leaving anything
		// else makes comparisons pointless because it'll never match. Omit
		// options.

		$ret = array();

		foreach($data as $datum) {
			if (!preg_match("/unmounted/", $datum))
				$ret[] = $datum;
		}

		return preg_replace("/^(\S+) (\w+).*\(([^;]*);.*$/",
		"<strong>$1</strong> $3 ($2)", $ret);
	}

	protected function preproc_export($data)
	{
		// Bold the export name, keep the export type, bin the rest
	
		return preg_replace("/^(\S+) (\(\w+\)).*$/", "<strong>$1</strong> $2",
		$data);
	}


}

//----------------------------------------------------------------------------
// APPLICATION AUDITS

class compareApp extends compareGeneric {

	protected $disp_name = "Application";

	protected $no_colour = array("AI server");

	private $cmp_vals = array();

	protected function preproc_generic($data)
	{
		// Split up the version and the path info
	
		return preg_replace("/@=(.*).*$/", " [$1]$2", $data);

	}

	protected function postproc_sshd($data)
	{
		// Don't colour if we're comparing SunSSH and OpenSSH. Just look at
		// the first letter 
	
		$cmp_arr = array();

		foreach($data as $row => $dat) {
			if (!empty($dat["a"])) $cmp_arr[] = $dat["a"][0];
			if (!empty($dat["b"])) $cmp_arr[] = $dat["b"][0];
		}

		$cmp_arr = array_unique($cmp_arr);

		return (count($cmp_arr) == 1)
			? $data
			: $this->uncolour($data);

	}

	protected function postproc_x_server($data)
	{
		// Don't colour if we have XSun. (We'd be either comparing to Xorg,
		// or always get an identical match as XSun doesn't report a
		// version.)

		$xsun = false;

		foreach($data as $row => $dat) {

			if (!isset($dat["a"])) $dat["a"] = false;
			if (!isset($dat["b"])) $dat["b"] = false;

			if (preg_match("/Xsun/", $dat["a"]) || preg_match("/Xsun/",
				$dat["b"])) {
				$xsun = true;
				break;
			}

		}

		return ($xsun)
			? $this->uncolour($data)
			: $data;

	}

	private function uncolour($data)
	{
		// strip colouring info out of an array

		$ret = array();
		$cols = array("ca" => true, "cb" => true);

		foreach($data as $datum) {
			$ret[] = array_diff_key($datum, $cols);
		}

		return $ret;
	}


	protected function get_highver($data)
	{
		// We may have two versions of python on one box and four on
		// another. This attempts to handle that by finding the highest
		// version. This information is fed back into the compare_generic()
		// method.
		
		$vers = array();

		foreach($data as $svr) {
			$vers = array_merge($vers, $svr);
		}

		// strip off the path, and sort. Get the highest version

		$vers = preg_replace("/\s*[\(\[].*$/", "", $vers);
		natsort($vers);

		return end($vers);
	}

}

//----------------------------------------------------------------------------
// TOOL AUDITS

class compareTool extends compareApp {

	protected $disp_name = "Tool";

	protected $no_colour = array();
}

//----------------------------------------------------------------------------
// SECURITY AUDITS

class compareSecurity extends compareGeneric {

	protected $sparse_list = array("user");

	protected $no_colour = array("user_attr", "root shell", "dtlogin",
	"cron job");

	protected function preproc_user_attr($data)
	{
		return $this->preproc_bold_first_word($data);
	}

	protected function preproc_dtlogin($data)
	{
		// Split up the version and the path info
	
		return preg_replace("/@=(.*).*$/", " [$1]$2", $data);

	}

	protected function preproc_cron_job($data)
	{
		// Cron jobs need HTMLizing. Also put the user in bold

		$ret = array();

		foreach($data as $datum) {
			$ret[] = htmlentities($datum);
		}

		return $this->preproc_bold_first_word($ret);
	}
}

//----------------------------------------------------------------------------
// HOSTED SERVICES AUDITS

class compareHosted extends compareGeneric {

	protected $disp_name = "Hosted Services";
}

//----------------------------------------------------------------------------
// PATCH AND PACKAGE AUDITS

class comparePatch extends compareGeneric {

	protected $disp_name = "Patch and Package";

	protected $sparse_list = array("patch", "package");
}

?>
